﻿using System;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Windows.Forms;
using System.Collections.Generic;
using NeuronDotNet.Controls;
using NeuronDotNet.Core;
using NeuronDotNet.Core.Backpropagation;
using NeuronDotNet.Samples.CharacterRecognition;
using NeuronDotNet.Samples.OCR.layoutsstategies;
using NeuronDotNet.Samples.OCR.Properties;

namespace NeuronDotNet.Samples.OCR
{
  public partial class MainForm : Form
  {
    private Alphabet currentLetter;

    private static readonly string[] letters = 
        {
            "A", "B", "C", "D", "E", "F", "G", "H", "I",
            "J", "K", "L", "M", "N", "O", "P", "Q", "R",
            "S", "T", "U", "V", "W", "X", "Y", "Z"
        };

    private static readonly string[] counts = 
        {
            "1","2","3","4","5"
        };

    #region constructor for Main class
    /// <summary>
    /// Set main configuration.
    /// </summary>
    public MainForm()
    {
      InitializeComponent();
    }
    #endregion

    #region private methods
    private void LoadLetter(object sender, EventArgs e)
    {
      currentLetter = Alphabet.GetLetter(cboAplhabet.SelectedIndex);
      SetLabels();
    }

    private void AddInstance(object sender, EventArgs e)
    {
      currentLetter.AddInstance(picTraining.Letter);
      SetLabels();
    }

    private void RemoveInstance(object sender, EventArgs e)
    {
      currentLetter.RemoveCurrentInstance();
      SetLabels();
    }

    private void RemoveAll(object sender, EventArgs e)
    {
      if (MessageBox.Show("Are you sure?", "Confirm Removal", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question) == DialogResult.Yes)
      {
        currentLetter.RemoveAll();
        SetLabels();
      }
    }

    private void ClearImage(object sender, EventArgs e)
    {
      btnClearImage.Enabled = false;
      btnAdd.Enabled = false;
      btnRemove.Enabled = false;
      picTraining.Letter = new Letter();
      picTraining.Invalidate();
      lblOf.Text = "New";
    }

    private void MoveNext(object sender, EventArgs e)
    {
      currentLetter.MoveNext();
      SetLabels();
    }

    private void MovePrevious(object sender, EventArgs e)
    {
      currentLetter.MovePrevious();
      SetLabels();
    }

    private void SetLabels()
    {
      if (currentLetter.InstancesCount > 0)
      {
        lblOf.Text = (currentLetter.InstanceIndex + 1) + " of " + currentLetter.InstancesCount;
        btnPrev.Enabled = true;
        btnNext.Enabled = true;
        btnClearImage.Enabled = true;
        btnRemoveAll.Enabled = true;
        btnRemove.Enabled = true;
        btnAdd.Enabled = true;
        picTraining.Letter = currentLetter.CurrentInstance;
      }
      else
      {
        lblOf.Text = "0 of 0";
        btnPrev.Enabled = false;
        btnNext.Enabled = false;
        btnClearImage.Enabled = false;
        btnRemove.Enabled = false;
        btnRemoveAll.Enabled = false;
        btnAdd.Enabled = false;
        picTraining.Letter = new Letter();
      }
      picTraining.Invalidate();
    }

    private void Clear(object sender, EventArgs e)
    {
      picCompressed.Invalidate();
      picRecognition.Letter = new Letter();
      picRecognition.Invalidate();
      lblResult.Visible = false;
      lblPreResult.Visible = false;
    }

    private void TrainingPicMousedown(object sender, MouseEventArgs e)
    {
      btnAdd.Enabled = true;
      btnRemove.Enabled = false;
      btnClearImage.Enabled = true;
      lblOf.Text = "New";
    }

    private void RecognitionPicMousedown(object sender, MouseEventArgs e)
    {
      lblResult.Visible = false;
      lblPreResult.Visible = false;
    }

    #region our methods
    /// <summary>
    /// Do Letters war and return better closer letter in string.
    /// </summary>
    private string RecognizeLetter(double[] letterVector)
    {
      //This var mean index in string[] letters(English alphabet).
      int winner = 0;
      int current = 1;

      ClearListBoxAndResultBufferedPanel();

      while (current < Alphabet.LetterCount)
      {
        try
        {
          using (Stream stream = File.Open(
            GeneratePathToNetwork(winner, current),
            FileMode.Open))
          {
            IFormatter formatter = new BinaryFormatter();
            var network = (INetwork)formatter.Deserialize(stream);
            double[] output = network.Run(letterVector);

            string result = String.Format(CultureInfo.InvariantCulture,
              Resource.vs,
              letters[winner],
              letters[current]);

            if (output[1] > output[0])
            {
              winner = current;
            }
            result += letters[winner];
            ShowLettersWarInListTextBox(result);
          }
          current++;
        }
        catch (Exception)
        {
          ShowFailedToLoadSavedNeuralNetworkErrorMessageBox();
        }
      }
      return letters[winner];
    }

    /// <summary>
    /// Recognize word.
    /// </summary>
    private void Recognize(object sender, EventArgs e)
    {
      HideResultAndPresultLabel();
      //divisionBuilder curry for division full image 
      //on single letter image.
      var divisionBuilder = new DivisionBuilder(picRecognition.Letter);
      //imageLayout can create list current clipped letters.
      var imageLayout = new ImageLayout(divisionBuilder);
      //in this var will accumulate recognized word.
      string word = String.Empty;
      List<Letter> letterList = imageLayout.GetClippedLetters();

      foreach (Letter letter in letterList)
      {
        //20x20 = 400 - the number of input neurons of every neural network
        double[] letterVector = letter.GetEquivalentVector(20, 20);
        word += RecognizeLetter(letterVector);
      }

      ShowResult(word);
    }

    #region our private methods
    /// <summary>
    /// This method hides Result And PreResult labels
    ///  on the user interface form.
    /// </summary>
    private void HideResultAndPresultLabel()
    {
      lblResult.Visible = false;
      lblPreResult.Visible = false;
    }

    /// <summary>
    /// This method displays the recognized word on the user interface form.
    /// </summary>
    private void ShowResult(string word)
    {
      lblResult.Text = word;
      lblResult.Visible = true;
      lblPreResult.Text = Resource.theWordIs;
      lblPreResult.Visible = true;
    }

    /// <summary>
    /// Show the result of comparison of two Letters in ListBox in format
    /// "first letter" vs "second letter" = "winner letter"
    /// (winner letter = first letter or second letter).
    /// </summary>
    private void ShowLettersWarInListTextBox(string result)
    {
      lstSimilarityTests.Items.Add(result);
      lstSimilarityTests.TopIndex = lstSimilarityTests.Items.Count -
        (lstSimilarityTests.Height / lstSimilarityTests.ItemHeight);
    }

    /// <summary>
    /// Generates Error MessageBox and error message:
    /// Failed to load saved neural networks.
    /// </summary>
    private static void ShowFailedToLoadSavedNeuralNetworkErrorMessageBox()
    {
      MessageBox.Show(Resource.failedToLoadErrorMessage,
            Resource.criticalError,
            MessageBoxButtons.OK,
            MessageBoxIcon.Error);
    }

    /// <summary>
    /// This method clears BufferedPanel where 
    /// user paints the word and ListBox where the recognized letters are shown. 
    /// </summary>
    private void ClearListBoxAndResultBufferedPanel()
    {
      lstSimilarityTests.Items.Clear();
      picCompressed.Invalidate();
    }

    /// <summary>
    /// Generates path to our network which can recognize letter.
    /// </summary>
    private static string GeneratePathToNetwork(int winner, int current)
    {
      string pathToNetwork = String.Format(CultureInfo.InvariantCulture,
        Resource.Path,
        Application.StartupPath,
        winner.ToString("00"),
        current.ToString("00")
        );
      return pathToNetwork;
    }
    #endregion
    #endregion

    private void LoadForm(object sender, EventArgs e)
    {
      cboAplhabet.Items.AddRange(counts);
      cboAplhabet.SelectedIndex = 0;
    }

    private void DrawCompressed(object sender, PaintEventArgs e)
    {
      double[] bitArray = picRecognition.Letter.GetEquivalentVector(20, 20);
      using (Bitmap map = new Bitmap(20, 20))
      {
        int k = 0;
        for (int i = 0; i < 20; i++)
        {
          for (int j = 0; j < 20; j++)
          {
            int darkness = (int)((1 - bitArray[k++]) * 255);
            map.SetPixel(j, i, Color.FromArgb(darkness, darkness, darkness));
          }
        }
        e.Graphics.DrawImage(map, 1, 1, 40, 40);
      }
    }

    private void TrainButtonClick(object sender, EventArgs e)
    {
      btnTrain.Enabled = false;
      int cycles = 200;
      if (!int.TryParse(txtCycles.Text, out cycles)) { cycles = 200; }
      txtCycles.Text = cycles.ToString();

      int currentCombination = 0;
      int totalCombinations = Alphabet.LetterCount * (Alphabet.LetterCount - 1) / 2;

      for (int i = 0; i < Alphabet.LetterCount; i++)
      {
        for (int j = i + 1; j < Alphabet.LetterCount; j++)
        {
          ActivationLayer inputLayer = new LinearLayer(400);
          ActivationLayer hiddenLayer = new SigmoidLayer(4);
          ActivationLayer outputLayer = new SigmoidLayer(2);
          new BackpropagationConnector(inputLayer, hiddenLayer);
          new BackpropagationConnector(hiddenLayer, outputLayer);
          BackpropagationNetwork network = new BackpropagationNetwork(inputLayer, outputLayer);

          TrainingSet trainingSet = new TrainingSet(400, 2);
          Alphabet ithLetter = Alphabet.GetLetter(i);
          Alphabet jthLetter = Alphabet.GetLetter(j);
          foreach (Letter instance in ithLetter.Instances)
          {
            trainingSet.Add(new TrainingSample(instance.GetEquivalentVector(20, 20), new double[] { 1d, 0d }));
          }
          foreach (Letter instance in jthLetter.Instances)
          {
            trainingSet.Add(new TrainingSample(instance.GetEquivalentVector(20, 20), new double[] { 0d, 1d }));
          }

          progressTraining.Value = 100 * currentCombination / totalCombinations;

          Application.DoEvents();

          bool correct = false;

          int currentCycles = 35;
          int count = trainingSet.TrainingSampleCount;

          while (correct == false & currentCycles <= cycles)
          {
            network.Initialize();
            network.Learn(trainingSet, currentCycles);
            correct = true;
            for (int sampleIndex = 0; sampleIndex < count; sampleIndex++)
            {
              double[] op = network.Run(trainingSet[sampleIndex].InputVector);
              if (((trainingSet[sampleIndex].OutputVector[0] > trainingSet[sampleIndex].OutputVector[1]) && op[0] - op[1] < 0.4) || ((trainingSet[sampleIndex].OutputVector[0] < trainingSet[sampleIndex].OutputVector[1]) && op[1] - op[0] < 0.4))
              {
                correct = false;
                trainingSet.Add(trainingSet[sampleIndex]);
              }
            }
            currentCycles *= 2;
          }

          lstLog.Items.Add(cboAplhabet.Items[i] + " & " + cboAplhabet.Items[j] + " = " + network.MeanSquaredError.ToString("0.0000"));
          lstLog.TopIndex = lstLog.Items.Count - (int)(lstLog.Height / lstLog.ItemHeight);
          try
          {
            using (Stream stream = File.Open(Application.StartupPath + @"\Networks\" + i.ToString("00") + j.ToString("00") + ".ndn", FileMode.Create))
            {
              IFormatter formatter = new BinaryFormatter();
              formatter.Serialize(stream, network);
            }
          }
          catch (Exception)
          {
            MessageBox.Show("Failed to save trained neural networks", "Critical Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
            return;
          }
          currentCombination++;
        }
      }
      progressTraining.Value = 0;
      btnTrain.Enabled = false;
    }
    #endregion
  }
}